package org.fitweb.fixture;

import fit.Fixture;
import fitlibrary.DoFixture;
import org.fitweb.ExtensionPoint;
import org.fitweb.drivers.BrowserController;
import org.fitweb.fixture.timing.Ready;
import org.fitweb.fixture.timing.TimedExecutor;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;

import java.io.FileNotFoundException;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Integer.valueOf;
import static java.lang.System.*;
import static java.util.concurrent.TimeUnit.*;
import static org.apache.commons.lang.StringUtils.*;
import static org.fitweb.FitwebProperties.*;

public class WebFixture extends DoFixture {

    protected BrowserController controller;
    protected final Map<String, String> userDefinedElements = new HashMap<String, String>() {{
        put("Page", "//body");
        put("Title", "//title");
    }};

    private HashMap<Integer, WebServerProvider> startedWebServers = new HashMap<Integer, WebServerProvider>();

    public WebFixture() {
        controller = new BrowserController(getProperty("browser"));
    }

    @SuppressWarnings("unused") // Fit
    public boolean sysout(String text) {
        System.out.println(text);
        return false;
    }

    @SuppressWarnings("unused") // Fit
    public boolean defineElementAs(String name, String xpath) {
        userDefinedElements.put(name, xpath);
        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean browse(String originalUrl) {
        String url = fixWindowsPath(substituteJavaProperties(originalUrl));
        try {
            controller.navigate().to(url);
        } catch (WebDriverException e) {
            Throwable cause = e.getCause();
            if (cause == null || !(cause instanceof FileNotFoundException)) { // handled below
                throw e;
            }
        }
        String currentUrl = controller.currentUrl();
        if (!deslash(url).equalsIgnoreCase(deslash(currentUrl))) {
            throw new DisplayableException("\nCannot see URL <b><u>" + url + "</u></b>\nActual URL is <b><u>" + currentUrl + "</u></b>");
        }
        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean dump(String elementName) {
        WebElement maybeElement = elementOrNull(elementName);
        showAfterTable(maybeElement != null ? contents(maybeElement) : contents("Page"));
        return false;
    }

    @SuppressWarnings("unused") // Fit
    public Fixture setFields() {
        return FieldsFixture.setFields(controller);
    }

    @SuppressWarnings("unused") // Fit
    public Fixture checkFields() {
        return FieldsFixture.checkFields(controller);
    }

    @SuppressWarnings("unused") // Fit
    public Fixture setRequestHeaders() {
        return new HttpRequestHeadersFixture(controller);
    }

    @SuppressWarnings("unused") // Fit
    public Fixture jsonValues() {
        return new JsonFixture(controller.pageContents());
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertStatusCodeIs(String statusCode) {
        if (controller.isHtmlUnit()) {
            return controller.getStatusCode() == valueOf(statusCode);
        } else {
            showAfterTable("Checking the status can only be done in HtmlUnit. Consider it a test of the server, not the client.");
        }

        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertContains(String elementName, String expectedResult) {
        String actualContents = contents(elementName);

        boolean contains = actualContents.contains(expectedResult);
        if (!contains) {
            showAfterTable("Expected <b>" + elementName + "</b> to contain \"<i>" + expectedResult + "</i>\" but was: \"<i>" + actualContents + "</i>\"");
        }

        return contains;
    }

    protected String contents(String elementName) {
        return contents(elementOrFail(elementName));
    }

    protected String contents(WebElement element) {
        String contents = isNotBlank(element.getValue()) ? element.getValue() : element.getText();
        return interceptString(contents);
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertDoesNotContain(String elementName, String expectedResult) {
        return !contents(elementName).contains(expectedResult);
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertContainsXml(String elementName, String expectedResult) {
        WebElement element = elementOrFail(elementName);

        String actualContents = new XmlFormatter(controller.innerHtml(element))
                .withUnnecessarySpacesRemoved()
                .withTagsUpperCase()
                .withLineBreaks()
                .withIndentation(4)
                .toString();

        expectedResult = new XmlFormatter(expectedResult)
                .withUnnecessarySpacesRemoved()
                .withTagsUpperCase()
                .withLineBreaks()
                .withIndentation(4)
                .toString();

        boolean contains = interceptString(actualContents).contains(expectedResult);
        if (!contains) {
            showAfterTable("Expected <b>" + elementName + "</b> to contain \"<i>" + expectedResult + "</i>\" but was: \"<i>" + actualContents + "</i>\"");
        }

        return contains;
    }

    @SuppressWarnings("unused") // Fit
    public boolean waitForToContain(final String elementName, final String expectedResult) {
        return when(new Ready<Boolean>() {
            protected boolean ready() {
                return contents(elementName).contains(expectedResult);
            }
        });
    }

    @SuppressWarnings("unused") // Fit
    public boolean waitForToBe(final String elementName, final String stateName) {
        return when(new Ready<Boolean>() {
            protected boolean ready() {
                return ElementState.called(stateName).appliesTo(elementOrNull(elementName));
            }
        });
    }

    @SuppressWarnings("unused") // Fit
    public boolean intoEnter(String elementName, String value) {
        WebElement webElement = elementOrFail(elementName);
        webElement.clear();
        webElement.sendKeys(value);
        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean click(String elementName) {
        WebElement webElement = elementOrFail(elementName);
        webElement.click();
        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertIs(String elementName, String stateName) {
        return ElementState.called(stateName).appliesTo(elementOrFail(elementName));
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertIsNot(String elementName, String stateName) {
        return !ElementState.called(stateName).appliesTo(elementOrFail(elementName));
    }

    @SuppressWarnings("unused") // Fit
    public boolean assertHasSize(String elementName, int expectedCount) {
        int actualCount;
        try {
            actualCount = controller.findElements(By.xpath(xpath(elementName))).size();
        } catch (NoSuchElementException e) {
            actualCount = 0;
        }

        boolean hasSize = actualCount == expectedCount;
        if (!hasSize) {
            showAfterTable("Expected <b>" + expectedCount + "</b> of \"<i>" + elementName + "</i>\" but was: <b>" + actualCount + "</b>");
        }
        return hasSize;
    }

    @SuppressWarnings("unused") // Fit
    public boolean setProxyToOnPort(String proxyServer, String proxyPort) {
        if (controller.isHtmlUnit()) {
            controller.setProxy(proxyServer, valueOf(proxyPort), property("proxy.username"), property("proxy.password"));
        } else {
            showAfterTable("For IE, Firefox and other non-HtmlUnit browsers, you should configure the proxy through the browser itself.");
        }

        return true;
    }

    @SuppressWarnings("unused") // Fit
    public boolean startServerOnPort(String canonicalClassName, int port) throws ClassNotFoundException {
        Class userProvidedClass = Class.forName(canonicalClassName);
        out.println("userProvidedClass = " + userProvidedClass);
        if (!WebServerProvider.class.isAssignableFrom(userProvidedClass)) {
            throw new IllegalArgumentException("The class you provided [" + userProvidedClass.getName() + "] does not implement " + WebServerProvider.class.getSimpleName());
        }

        Object instance;
        try {
            Constructor constructor = userProvidedClass.getConstructor();
            instance = constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        startedWebServers.put(port, ((WebServerProvider) instance));

        return ((WebServerProvider) instance).start(port);
    }

    @SuppressWarnings("unused") // Fit
    public boolean stopServerOnPort(int port) throws ClassNotFoundException {
        if (startedWebServers.containsKey(port)) return startedWebServers.remove(port).stop();
        throw new RuntimeException("No server has been started on port " + port);
    }

    @SuppressWarnings("unused") // Fit
    public Fixture summary() {
        return new TidySummaryFixture();
    }

    private boolean when(Ready<Boolean> ready) {
        try {
            TimedExecutor.<Boolean>waitFor(10, SECONDS).on(ready);
            return true;
        } catch (TimeoutException e) {
            return false;
        }
    }

    private WebElement elementOrFail(String elementName) {
        WebElement element;
        try {
            element = controller.findElement(By.xpath(xpath(elementName)));
        } catch (NoSuchElementException e) {
            throw new DisplayableException("Cannot find <b>'" + elementName + "'</b>\n" + e.getMessage());
        }
        return element;
    }

    private WebElement elementOrNull(String elementName) {
        try {
            return controller.findElement(By.xpath(xpath(elementName)));
        } catch (NoSuchElementException e) {
            return null;
        }
    }

    private String xpath(String name) {
        String xpath = userDefinedElements.get(name);
        if (xpath == null) {
            throw new DisplayableException("No element defined for <b>'" + name + "'</b>");
        }
        return xpath;
    }

    String substituteJavaProperties(String originalString) {
        Matcher matcher = Pattern.compile("\\$\\{(.*?)\\}").matcher(originalString);
        while (matcher.find()) {
            String matchingText = matcher.group(1);
            String propertyValue = Matcher.quoteReplacement(getProperty(matchingText));
            originalString = originalString.replaceAll("\\$\\{" + matchingText + "\\}", propertyValue);
        }

        return originalString;
    }

    String deslash(String url) {
        return url.replaceAll("[/]+", "/");
    }

    String fixWindowsPath(String originalPath) {
        return originalPath.replaceAll("\\\\", "/");
    }

    @ExtensionPoint
    protected String interceptString(String someText) {
        return someText;
    }

}